package com.ideaaedi.commonds.security;

import com.ideaaedi.commonds.constants.AlgorithmConstant;
import com.ideaaedi.commonds.exception.CryptographyRuntimeException;
import lombok.experimental.UtilityClass;

import javax.annotation.concurrent.ThreadSafe;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import static com.ideaaedi.commonds.security.Base64.encode;

/**
 * AES加解密
 *
 * @author <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
 * @since 1.0.0
 */
@ThreadSafe
@UtilityClass
@SuppressWarnings("AlibabaLowerCamelCaseVariableNaming")
public final class AES extends BaseSecuritySupport {
    
    /**
     * @see AES#generateKey()
     */
    public static String generateKeySilently() {
        try {
            return AES.generateKey();
        } catch (NoSuchAlgorithmException e) {
            throw new CryptographyRuntimeException(e);
        }
    }
    
    /**
     * 获取AES密钥
     *
     * @throws NoSuchAlgorithmException 当算法不存在时抛出
     * @return  BASE64后的获取AES密钥
     */
    public static String generateKey() throws NoSuchAlgorithmException {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(AlgorithmConstant.AES);
        keyGenerator.init(256);
        SecretKey secretKey = keyGenerator.generateKey();
        return encode(secretKey.getEncoded());
    }
    
    /**
     * @see AES#encrypt(String, String)
     */
    public static String encryptSilently(String content, String aesKey) {
        try {
            return AES.encrypt(content, aesKey);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException | InvalidKeyException e) {
            throw new CryptographyRuntimeException(String.format("content -> %s, aesKey -> %s", content, aesKey), e);
        }
    }
    
    /**
     * 加密
     *
     * @param plainText
     *            明文
     * @param aesKey
     *            密钥
     * @throws   NoSuchAlgorithmException 当算法不存在时抛出
     * @throws   NoSuchPaddingException 当填充模式不存在时抛出
     * @throws   InvalidKeyException 当密钥不合法时抛出
     * @throws   IllegalBlockSizeException IllegalBlockSizeException
     * @throws   BadPaddingException BadPaddingException
     * @return 密文
     */
    public static String encrypt(String plainText, String aesKey) throws NoSuchAlgorithmException, NoSuchPaddingException,
            InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
        byte[] aesKeyBytes = decode(aesKey);
        SecretKeySpec secretKeySpec = new SecretKeySpec(aesKeyBytes, AlgorithmConstant.AES);
        Cipher cipher = Cipher.getInstance(AlgorithmConstant.AES_GCM_NoPadding);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
    
        byte[] contentBytes = plainText.getBytes(StandardCharsets.UTF_8);
        // 加密得到密文
        byte[] cipherContentBytes = cipher.doFinal(contentBytes);
    
        assert cipherContentBytes.length == contentBytes.length + 16;
        byte[] finalCipherContentBytes = new byte[cipherContentBytes.length + 12];
        // 向量，以保证没每次即便对同一明文加密，密文都不一样
        byte[] iv = cipher.getIV();
        assert iv.length == 12;
        System.arraycopy(iv, 0, finalCipherContentBytes, 0, iv.length);
        System.arraycopy(cipherContentBytes, 0, finalCipherContentBytes, 12, cipherContentBytes.length);
        return encode(finalCipherContentBytes);
    }
    
    /**
     * @see AES#encrypt(String, String)
     */
    public static String decryptSilently(String cipherText, String aesKey) {
        try {
            return AES.decrypt(cipherText, aesKey);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException
                | BadPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new CryptographyRuntimeException(String.format("cipherText -> %s, aesKey -> %s", cipherText, aesKey), e);
        }
    }
    
    /**
     * 解密
     *
     * @param cipherText
     *            密文
     * @param aesKey
     *            密钥
     * @throws   NoSuchAlgorithmException 当算法不存在时抛出
     * @throws   NoSuchPaddingException 当填充模式不存在时抛出
     * @throws   InvalidKeyException 当密钥不合法时抛出
     * @throws   InvalidAlgorithmParameterException 当算法参数不合法时抛出
     * @throws   IllegalBlockSizeException IllegalBlockSizeException
     * @throws   BadPaddingException BadPaddingException
     * @return 明文
     */
    public static String decrypt(String cipherText, String aesKey)
            throws NoSuchAlgorithmException, NoSuchPaddingException,
            InvalidKeyException, IllegalBlockSizeException,
            BadPaddingException, InvalidAlgorithmParameterException {
        byte[] aesKeyBytes = decode(aesKey);
        byte[] cipherTextBytes = decode(cipherText);
        if (cipherTextBytes.length < 12 + 16) {
            throw new CryptographyRuntimeException("cipherText is illegal.");
        }
        SecretKeySpec secretKeySpec = new SecretKeySpec(aesKeyBytes, AlgorithmConstant.AES);
        Cipher cipher = Cipher.getInstance(AlgorithmConstant.AES_GCM_NoPadding);
        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, cipherTextBytes, 0, 12);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec);
        byte[] plainTetBytes = cipher.doFinal(cipherTextBytes, 12, cipherTextBytes.length - 12);
        return new String(plainTetBytes, StandardCharsets.UTF_8);
    }
    
    
    /**
     * @see AES#simpleEncrypt(String, String)
     */
    public static String simpleEncryptSilently(String content, String aesKey) {
        try {
            return AES.simpleEncrypt(content, aesKey);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException | InvalidKeyException e) {
            throw new CryptographyRuntimeException(String.format("content -> %s, aesKey -> %s", content, aesKey), e);
        }
    }

    /**
     * 加密
     *
     * @param plainText
     *            明文
     * @param aesKey
     *            密钥
     * @throws   NoSuchAlgorithmException 当算法不存在时抛出
     * @throws   NoSuchPaddingException 当填充模式不存在时抛出
     * @throws   InvalidKeyException 当密钥不合法时抛出
     * @throws   IllegalBlockSizeException IllegalBlockSizeException
     * @throws   BadPaddingException BadPaddingException
     * @return 密文
     */
    public static String simpleEncrypt(String plainText, String aesKey)
            throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException,
            IllegalBlockSizeException, BadPaddingException {
        byte[] aesKeyBytes = decode(aesKey);
        Cipher cipher = Cipher.getInstance(AlgorithmConstant.AES_ECB_PKCS5Padding);
        SecretKey secretKey = new SecretKeySpec(aesKeyBytes, AlgorithmConstant.AES);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        byte[] encryptedData = cipher.doFinal(plainText.getBytes());
        return encode(encryptedData);
    }
    
    /**
     * @see AES#simpleDecrypt(String, String)
     */
    public static String simpleDecryptSilently(String cipherText, String aesKey) {
        try {
            return AES.simpleDecrypt(cipherText, aesKey);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException
                | BadPaddingException | InvalidKeyException e) {
            throw new CryptographyRuntimeException(String.format("cipherText -> %s, aesKey -> %s", cipherText, aesKey), e);
        }
    }
    
    /**
     * 解密
     *
     * @param cipherText
     *            密文
     * @param aesKey
     *            密钥
     * @throws   NoSuchAlgorithmException 当算法不存在时抛出
     * @throws   NoSuchPaddingException 当填充模式不存在时抛出
     * @throws   InvalidKeyException 当密钥不合法时抛出
     * @throws   IllegalBlockSizeException IllegalBlockSizeException
     * @throws   BadPaddingException BadPaddingException
     * @return 明文
     */
    public static String simpleDecrypt(String cipherText, String aesKey)
            throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException,
            IllegalBlockSizeException, BadPaddingException {
        byte[] aesKeyBytes = decode(aesKey);
        Cipher cipher = Cipher.getInstance(AlgorithmConstant.AES_ECB_PKCS5Padding);
        SecretKey secretKey = new SecretKeySpec(aesKeyBytes, AlgorithmConstant.AES);
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        byte[] contentAfterBase64 = decode(cipherText);
        byte[] decryptedData = cipher.doFinal(contentAfterBase64);
        return new String(decryptedData, StandardCharsets.UTF_8);
    }
    
    /**
     * base64#decode
     */
    private static byte[] decode(String aesKey) {
        return Base64.decode(aesKey);
    }
    
}
